package cn.tannn.surenessdemo.sureness.processor;

import cn.tannn.surenessdemo.enums.ExceptionEnum;
import cn.tannn.surenessdemo.exception.ExpiredRedisException;
import cn.tannn.surenessdemo.sureness.provider.DatabaseAccountProvider;
import cn.tannn.surenessdemo.util.JwtTokenUtil;
import com.usthe.sureness.processor.BaseProcessor;
import com.usthe.sureness.processor.exception.*;
import com.usthe.sureness.subject.PrincipalMap;
import com.usthe.sureness.subject.Subject;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.subject.support.JwtSubject;
import com.usthe.sureness.subject.support.SinglePrincipalMap;
import com.usthe.sureness.util.JsonWebTokenUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
 * token鉴权 - header:token
 * @author tan
 * @date 2020-12-03 20:37
 */
public class CustomRedisProcessor extends BaseProcessor {
    private static final Logger logger = LoggerFactory.getLogger(CustomRedisProcessor.class);


    private DatabaseAccountProvider accountProvider;


    /**
     * 复用 JwtSubject
     */
    @Override
    public boolean canSupportSubjectClass(Class<?> var) {
        return var == JwtSubject.class;
    }

    /**
     * 复用 JwtSubject
     */
    @Override
    public Class<?> getSupportSubjectClass() {
        return JwtSubject.class;
    }


    @Override
    public SubjectSum process(Subject var) throws SurenessAuthenticationException, SurenessAuthorizationException {
        // 刷新资源树一定要第一个加载
        this.rebuildTree();
        Subject subject = this.authenticated(var);
        this.authorized(subject);
        this.refreshToken(subject);
        return subject.generateSubjectSummary();
    }


    @Override
    public Subject authenticated(Subject subject) throws SurenessAuthenticationException {
        String jwt = (String) subject.getCredential();
        if (JsonWebTokenUtil.isNotJsonWebToken(jwt)) {
            throw new  IncorrectCredentialsException("token是非法的");
        }

        Claims claims;
        try {
            // token 解析
            claims = JwtTokenUtil.parseJwtNoExpiredJwtException(jwt);
            // redis 中比对 token 正确性
            String tokenRedis = accountProvider.redisTemplate().verifyUserToken(claims.getSubject());
            if(!tokenRedis.equals(jwt)){
                throw new ExpiredRedisException(ExceptionEnum.REDIS_NO_USER);
            }
            // todo 用户状态鉴定
            accountProvider.redisTemplate().verifyUserStatus(claims.getSubject());
        } catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
            // JWT error
            if (logger.isDebugEnabled()) {
                logger.debug("jwtProcessor 身份验证失败, user: {}, jwt: {}",
                        subject.getPrincipal(), jwt);
            }
            throw new IncorrectCredentialsException("这个 jwt 错误:" + e.getMessage());
        } catch (ExpiredRedisException e) {
            // redis中数据没了
            if (logger.isDebugEnabled()) {
                logger.debug("redis authenticated expired, user: {}, jwt: {}",
                        subject.getPrincipal(), jwt);
            }
            throw new ExpiredCredentialsException(e.getMessage());
        }catch (DisabledAccountException | ExcessiveAttemptsException e){
            // 用户有问题
            if (logger.isDebugEnabled()) {
                logger.debug("user disabled or excessiveAttempts, user: {}, jwt: {}",
                        subject.getPrincipal(), jwt);
            }
            throw e;
        }
        // 设置用户的唯一值到 principal
        subject.setPrincipal(claims.getSubject());


        // todo redis-memory 用户鉴权从redis中获取
        // 权限从数据库获取而不是用jwt中的权限,获取修改权限后强制下线(多个服务之间使用共同的权限和登录时考虑强迫被修改的用户退出)
        // 此处做无感刷新权限（从数据库拿去权限) <code>List<String> ownRoles = claims.get("roles", List.class);</code>
        // 主这里用 redis mysql, 压力会很大 <code>  List<String> ownRoles = accountProvider.loadAccountRoles(claims.getSubject());</code>
        List<String> ownRoles = accountProvider.redisTemplate().loadUserRole(claims.getSubject());
        if (nonNull(ownRoles)) {
            subject.setOwnRoles(ownRoles);
        }
        PrincipalMap principalMap = new SinglePrincipalMap();
        for (Map.Entry<String, Object> claimEntry : claims.entrySet()) {
            principalMap.setPrincipal(claimEntry.getKey(), claimEntry.getValue());
        }
        subject.setPrincipalMap(principalMap);
        return subject;
    }


    @Override
    public void authorized(Subject sub) throws SurenessAuthorizationException {
        // 在上面的 90行 处设置的
        List<String> ownRoles = (List<String>) sub.getOwnRoles();
        List<String> supportRoles = (List<String>) sub.getSupportRoles();
        // supportRoles是当前资源拥有的权限,如果当前资源无权限，则放行(严谨的话这里时不允许发行，资源要么前置放行了，要么就必须要有基础角色)
        if (supportRoles == null) {
            return;
        }
        // ownRoles使用户所拥有的权限，supportRoles是当前资源拥有的权限，用户跟资源的权限有一样那就放他过
        if (ownRoles != null && supportRoles.stream().anyMatch(ownRoles::contains)) {
            return;
        }
        throw new UnauthorizedException("无权访问，请给用户设置角色 ");
    }


    /**
     * todo token续签
     * @param sub Subject
     */
    public void refreshToken(Subject sub){
        try {
            accountProvider.redisTemplate().refreshUserToken(sub.getPrincipal().toString());
        }catch (Exception e){
            throw new IncorrectCredentialsException("token续签异常:" + e.getMessage());
        }
    }

    /**
     * todo redis-memory 每次认证前都刷新一次资源数（从redis中获取元数据刷到内存中
     * 刷新资源树 （多个服务共同使用同一权限时用，或者不想到处刷新资源树时用）
     * 用户每次验证后都从redis中获取数据来刷新本地资源树，保证内存与redis中的数据一致
     */
    public void rebuildTree(){
        try {
            accountProvider.treePathRoleMatcherServiceTemplate().rebuildTreeRepetition();
        }catch (Exception e){
            throw new IncorrectCredentialsException("资源树刷新异常:" + e.getMessage());
        }
    }


    public void setAccountProvider(DatabaseAccountProvider accountProvider) {
        this.accountProvider = accountProvider;
    }
}
